import exceptions
#import xml.etree.ElementTree as etree
from lxml import etree

version = "0.0.1"

_namespaces = {
    "l"    : "ddi:logicalproduct:3_1",                      
    "ns1"  : "ddi:instance:3_1" ,
    "a"    : "ddi:archive:3_1" ,
    "r"    : "ddi:reusable:3_1" ,
    "dc"   : "ddi:dcelements:3_1" ,
    "cm"   : "ddi:comparative:3_1" ,
    "d"    : "ddi:datacollection:3_1" ,
    "l"    : "ddi:logicalproduct:3_1" ,
    "c"    : "ddi:conceptualcomponent:3_1" ,
    "ds"   : "ddi:dataset:5_1" ,
    "p"    : "ddi:physicaldataproduct:3_1" ,
    "pr"   : "ddi:ddiprofile:3_1" ,
    "s"    : "ddi:studyunit:3_1" ,
    "g"    : "ddi:group:3_1" ,
    "pi"   : "ddi:physicalinstance:3_1" ,
    "m3"   : "ddi:physicaldataproduct_ncube_inline:3_1" ,
    "m1"   : "ddi:physicaldataproduct_ncube_normal:3_1" ,
    "m2"   : "ddi:physicaldataproduct_ncube_tabular:3_1",
    "xml"  : "http://www.w3.org/XML/1998/namespace"
}
ns = _namespaces
ns1 = dict()
for x,y in ns.items():
    ns1[y] = x
# The following line should work, but doesn't yet...
#etree._namespace_map.update(ns1)

class ddi_CC_wrapper():
    def __init__(self, filename = None, id = None):
        self.ddi = etree.ElementTree()
        self.filename = filename
        
        parser = etree.XMLParser(encoding='utf-8')

        if self.filename != None:
            self.filename = str(self.filename)
            self.ddi.parse(self.filename,parser=parser)
        else:
            self.ddi = etree.fromstring(self.newDDIfile(),parser=parser)
            self.ddi.set('id',id) 
#            self.ddi.set('agency',agency)
 #           self.ddi.set('version',version)
            self.ddi = etree.ElementTree(element=self.ddi)

    """
    This will return a DDI Instance, with a StudyUnit with an empty LogicalProduct
    """
    def newDDIfile(self):
        with open('libVirgil/templates/ddi31.xml') as d:
            return d.read()

    def getTopLevelCodeLists(self):
        topCodes = []
        for cs in self.ddi.findall(".//{%s}CodeScheme"%(ns["l"])):
            id    = cs.attrib.get("id")
            label = cs.findall(".//{%s}Label"%(ns["r"]))
            labels = self._getMultilingualLabels(cs)
            if len(label) > 0:
                value = "*"
                label = label[0].text
            else:
                value = "*"
                label = "No Label"
            yield {"id":id,"value":value,"label":label,"labels":labels}

    # Recursively read code and make a pretty print of the code
    def getCodeAsText(self,id,lang,depth=0):
        items = self.getCodeAsTuples(id,lang,depth)
        longestCode = len(max(zip(*items)[1],key=len))
        out = ""
        for indent,val,label in items:
            padding= max(longestCode - len(val),0)
            out += "%s%s %s. %s\n"%(indent,val,"."*padding,label)
        return out

    """
    Optimised way to get the single label with a specific language for a category
    """
    def _getCategoryLabelLang(self,catref,lang):
        cat = self.ddi.find(".//{%s}Category[@id='%s']/{%s}Label[@{%s}lang='%s']"%(ns['l'],catref,ns['r'],ns['xml'],lang))
        if cat is not None:
            return self._mixedHTMLReusable(cat)
        else:
            return "No Translation for %s"%lang

    def getCodeAsTuples(self,id,lang,depth=0,indent=0):
        catref = self.getCategoryFromCode(id)['id']
        val  = self.getCode(id)['value']
        codes = [(id,depth,catref,val)]
        items = []
        while codes:
            code, dep, catref, val = codes.pop()
            label = self._getCategoryLabelLang(catref,lang)
            items += [(" "*(depth-dep),val,label)]
            if dep > 0:
                children = self.getChildCodes(code)
                children.reverse()
                for cid,val,catref in children:
                    codes.append((cid,dep-1,catref,val))
        return items
        
    def getChildCodes(self,code):
        children = []
        for child in self.ddi.findall(".//*[@id='%s']/{%s}Code"%(code,ns["l"])):
            id = child.attrib.get("id")
            value = child.find(".//{%s}Value"%(ns["l"])).text
            catref = child.find(".//{%s}CategoryReference/{%s}ID"%(ns["l"],ns["r"])).text
            children.append((id,value,catref))
        return children
    """
    Finds the notation of a code from a given id
    ARGS:       code_id - string - ID of code to find
    RETURNS:              string - notation of given code
    """
    def getCode(self,code_id):
        xml = self.ddi.find(".//{%s}Code[@id='%s']"%(ns["l"],code_id))
        code = dict()
        code['value'] = xml.find(".//{%s}Value"%(ns["l"])).text
        return code 

    """
    For the time being we fake this, and just return the string '*' regardless of the passed param
    ARGS:       code_id - string - ID of code to find
    RETURNS:              string - notation of given code
    """
    def getCodeSchemeAsCode(self,code_id):
        code = dict()
        code['value'] = "*"

        return code 
#    def getCategory(self,code_id):
#        catref = self.ddi.find(".//{%s}Code[@id='%s']/{%s}CategoryReference/{%s}ID"%(ns["l"],code_id,ns["l"],ns["r"])).text
#        cat = self.ddi.find(".//*[@id='%s']"%(catref))
#        return cat 


    def getCategoryFromCode(self,code_id):
        catref = self.ddi.find(".//{%s}Code[@id='%s']/{%s}CategoryReference/{%s}ID"%(ns["l"],code_id,ns["l"],ns["r"])).text
        return self.getCategory(catref)

    # There is no category for a CodeScheme, but we want to edit its label and description in the same way, so we are going to 'fake it' (TM)
    def getCategoryFromCodeScheme(self,id):
        cs = self.ddi.find(".//{%s}CodeScheme[@id='%s']"%(ns["l"],id))
        labels,descs = self._getMultilingualDict(cs)
        cs = {'id': None, "labels":labels, "descriptions":descs}
        return cs 
    
    """
    For a given category ID, finds the corresponding category
    ARGS:       category - string - ID of category to get category for
    RETURNS:            - dict   - contains labels and descriptions
                            { id           : "CATEGORY_ID"
                              labels       : { "en-au" : "Australian Label", "de" - "Generic German Label" },
                              descriptions : { "en-au" : "Australian Description", "de" - "Generic German Description" }
                            }
    """
    def getCategory(self,catid):
        cat = self.ddi.find(".//{%s}Category[@id='%s']"%(ns['l'],catid))
        labels,descs = self._getMultilingualDict(cat)
        cat = {'id': catid, "labels":labels, "descriptions":descs}
        return cat 

    def _getMultilingualLabelsAsText(self,item):
        labels = dict() 
        for label in item.findall(".//{%s}Label"%ns['r']):
            lang = label.get("{%s}lang"%ns['xml'], "none")
            labels[lang] = self.label.text
        return labels
    def _getMultilingualLabels(self,item):
        labels = dict() 
        for label in item.findall(".//{%s}Label"%ns['r']):
            lang = str(label.get("{%s}lang"%ns['xml'], "none"))
            labels[lang] = self._mixedHTMLReusable(label)
        return labels

    def _getMultilingualDescriptions(self,item):
        descs  = dict()
        for desc in item.findall(".//{%s}Description"%ns['r']):
            lang = str(desc.get("{%s}lang"%ns['xml'], "none"))
            descs[lang] = self._mixedHTMLReusable(desc)
        return descs

    def _getMultilingualDict(self,item):
        return (self._getMultilingualLabels(item),self._getMultilingualDescriptions(item))

    # Take a XHTML & text tag (eg. Label, Description, etc) and return just the innerbit
    def _mixedHTMLReusable(self,tag):
        #tag = etree.tostring(tag)
        tag = tag.text
        if tag is not None:
            tag = tag.replace('<xhtml:','<').replace('<html:','<') # remove XHTML namespaces
        else:
            tag = u""
        return tag
        
    def updateCodeCategory(self,obj):
        code_id = obj['code']
        value = obj['value']
        category = obj['category']
        
        self.updateCodeValue(code_id,value)
        if category['id'] != None:
            self.updateCategory(category) 
        else:
            self.updateCodeSchemeLabelDesc(code_id,category)

    def updateCodeValue(self,code_id,value):
        val = self.ddi.find(".//{%s}Code[@id='%s']/{%s}Value"%(ns["l"],code_id,ns["l"]))
        if val != None:
            val.text = value
        
    def insertViaWaypoint(self,item,elem,waypoints):
        children = [ child.tag for child in item ]
        waypoints.reverse() #Lets start in the most likely place. Next to a The item iself, and move backwards
        index = -1 #If we can't find the right place to put the element, it can safely go at the start cause nothing comes before it.
        for w in waypoints: 
            try:
                if index == -1:
                    index = children.index(w)
            except:
                pass 
        item.insert(index+1,elem)  # We add one to add the new item AFTER the waypoint

    def updateLabelsOrDescriptions(self,item,element,texts,waypoints):
        for lang, text in texts.items():
            elem = item.find(".//{%s}%s[@{%s}lang='%s']"%(ns['r'],element,ns['xml'],lang))
            lang = str(lang) # this is a bad fix. Why does this show up as a language: PyQt4.QtCore.QString(u'en-au') TODO
            if elem is None:
                elem = etree.Element("{%s}%s"%(ns['r'],element))
                elem.set("{%s}lang"%ns['xml'], lang)
                self.insertViaWaypoint(item,elem,waypoints)
#            else:
#                attrs = elem.attrib.copy()
#                elem.clear()
#                for key,val in attrs.items():
#                    elem.set(key,val)
#            print text
            elem.text = text

    def updateLabels(self,item,labels):
        waypoints = [
            "{%s}UserID"%ns['r'],
            "{%s}VersionResponsibility"%ns['r'],
            "{%s}VersionRationale"%ns['r'],
            "%sName"%item.tag, #Most elements have a ElementName name child (where Element is the Element title)
            "{%s}Label"%ns['r'],
        ] #this is a list of possible elements that will help us 'find' where to put the text
        self.updateLabelsOrDescriptions(item,'Label',labels,waypoints)
        
    def updateDescriptions(self,item,descriptions):
        waypoints = [
            "{%s}UserID"%ns['r'],
            "{%s}VersionResponsibility"%ns['r'],
            "{%s}VersionRationale"%ns['r'],
            "%sName"%item.tag, #Most elements have a ElementName name child (where Element is the Element title)
            "{%s}Label"%ns['r'],
            "{%s}Description"%ns['r'],
        ] #this is a list of possible elements that will help us 'find' where to put the text
        self.updateLabelsOrDescriptions(item,'Description',descriptions,waypoints)

    def updateCodeSchemeLabelDesc(self, code, category):
        id = code
        labels = category['labels']
        descriptions = category['descriptions']
        cs = self.ddi.find(".//{%s}CodeScheme[@id='%s']"%(ns['l'],id))
        self.updateLabels(cs,labels)
        self.updateDescriptions(cs,descriptions)

    def updateCategory(self, category):
        id = category['id']
        labels = category['labels']
        descriptions = category['descriptions']
        cat = self.ddi.find(".//{%s}Category[@id='%s']"%(ns['l'],id))
        self.updateLabels(cat,labels)
        self.updateDescriptions(cat,descriptions)
    

    # langs is a hash with key being the iso 3 letter language, and the value being a hash with keys of (label, description) for the given language
    def newCategoryScheme(self,id,labels=None,descriptions=None):
        if id == None:
            raise InvalidDDI31IDError
        cs = etree.Element('{%s}CategoryScheme'%ns['l'])
        cs.set('id',id)
        
        if labels is not None:
            self.updateLabels(cs,labels)
        if descriptions is not None:
            self.updateLabels(cs,descriptions)

        lp = self.ddi.find(".//{%s}LogicalProduct"%(ns['l']))
        lp.append(cs)

    def newCodeScheme(self,id,catSchemeID = None,labels=None,descriptions=None):
        if id == None:
            raise InvalidDDI31IDError
        cs = etree.Element('{%s}CodeScheme'%ns['l'])
        cs.set('id',id)

        lp = self.ddi.find(".//{%s}LogicalProduct"%(ns['l']))
        lp.append(cs)
        if catSchemeID is not None:
            csRef = etree.Element('{%s}CategorySchemeReference'%ns['r']) 
            ref = etree.Element('{%s}ID'%ns['r'])
            ref.text = catSchemeID
            csRef.append(ref)
            cs.append(csRef)
        if labels is not None:
            self.updateLabels(cs,labels)
        if descriptions is not None:
            self.updateDescriptions(cs,descriptions)

    def newClassification(self,codeSID,categorySID,labels,descriptions):
        self.newCategoryScheme(categorySID,labels=labels,descriptions=descriptions)
        self.newCodeScheme(codeSID,categorySID,labels=labels,descriptions=descriptions)
        return self.getCategoryFromCodeScheme(codeSID)

    # Adds a new category/code to the DDI from a dict with the example, structure:
    """
                            { code               : "01",
                              parent             : "00",
                              code_id            : "Code_ID_01",
                              category_id        : "Category_ID_01",
                              labels             : { "en-au" : "Australian Label", "de" : "Generic German Label" },
                              descriptions       : { "en-au" : "Australian Description", "de" : "Generic German Description" }
                            }
    """
    def newCodeCategory(self,classn, codescheme_id, categoryscheme_id):

        code = etree.Element('{%s}Code'%ns['l'])
        code.set('id',classn['code_id'])

        catref = etree.Element('{%s}CategoryReference'%ns['l'])
        value = etree.Element('{%s}Value'%ns['l'])
        value.text = classn['code']

        id_ref   = etree.Element('{%s}ID'%ns['r'])
        id_ref.text = classn['category_id']

        catref.append(id_ref)
        code.append(catref)
        code.append(value)

        category = etree.Element('{%s}Category'%ns['l'])
        category.set('id',classn['category_id'])
        self.updateLabels(category,classn['labels'])
        self.updateDescriptions(category,classn['descriptions'])

        codeScheme     = self.ddi.find(".//{%s}CodeScheme[@id='%s']"%(ns['l'],codescheme_id))
        categoryScheme = self.ddi.find(".//{%s}CategoryScheme[@id='%s']"%(ns['l'],categoryscheme_id))
        if classn['parent'] == codescheme_id:
            parent     = codeScheme
        else:
            parent     = codeScheme.find(".//{%s}Code[@id='%s']"%(ns['l'],classn['parent']))

        parent.append(code)
        categoryScheme.append(category)

    # Return all possible languages for the category labels
    def labelLanguages(self):
        langs = set()
        for label in self.ddi.findall(".//{%s}Category/{%s}Label"%(ns['l'],ns['r'])):
            lang = label.get("{%s}lang"%ns['xml'])
            langs.add(lang)

        langs.add(None)     # Make sure we have a None to remove
        langs.remove(None)  # This looks silly, but it removes 'None' objects from the set, these may appear if a label doesnt have an xml:lang attribute (which would be bad practice anyway :P)
        return langs
        
    def setCodeParent(self,childID,parentID,oldParentID,index):
        child  = self.ddi.find(".//*[@id='%s']"%(childID))
        parent = self.ddi.find(".//*[@id='%s']"%(parentID))
        oldparent = self.ddi.find(".//*[@id='%s']"%(oldParentID))  
        oldparent.remove(child)        # Remove the child from the old parent
        parent.insert(index,child)     # Move the child to the new parent

    #Find the code scheme this category lives in
    def getCodeSchemeFromCode(self,codeID):
        codeScheme     = self.ddi.xpath("//l:CodeScheme[//l:Code/@id='%s']"%codeID,namespaces=_namespaces)
        #if we can't find a parent code scheme, its not a code. So we assume it must be a codescheme
        if len(codeScheme) == 0:
            # But if this fails someone has called the wrong method, so we'll let it fail.
            codeScheme     = self.ddi.xpath("//l:CodeScheme[@id='%s']"%codeID,namespaces=_namespaces)
            
        return codeScheme[0].get('id')

    # Finds the most likely category scheme this code belongs to.
    # Returns the value of the first CategorySchemeReference" it finds, or the the first CategoryScheme in the LogicalProduct
    def getCategorySchemeFromCode(self,codeID):
        codeSchemeID = self.getCodeSchemeFromCode(codeID)
        codeScheme   = self.ddi.xpath("//l:CodeScheme[@id='%s']"%codeSchemeID,namespaces=_namespaces)[0]
        catSchemeRef = codeScheme.xpath("/l:CategorySchemeReference",namespaces=_namespaces)
        if len(catSchemeRef) > 0:
            catSchemeRef = cateSchemeRef[0].xpath("/r:ID",namespaces=_namespaces)
        else:
            # no category scheme reference is defined, so we'll fix the first one in the logical product
            catSchemeRef = self.ddi.xpath("//l:CategoryScheme",namespaces=_namespaces)[0].get('id')
            
        return catSchemeRef


    def deleteItem(self,ID):
        item   = self.ddi.xpath("//*[@id='%s']"%ID,namespaces=_namespaces)[0]
        parent = item.getparent()
        parent.remove(item)

    # Takes a filename, serialises the DDI and writes it out as DDI3.1
    def saveToFile(self, file):
        self.ddi.write(file,encoding='utf-8')

    def toString(self):
        return etree.tostring(self.ddi, encoding="utf-8")

class InvalidDDI31IDError(exceptions.Exception):
    def __init__(self,output):
        self.InvalidID = id
        return

    def __str__(self):
        print "","Invalid id - %s" % self.InvalidID
